﻿using System;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Windows.Forms;
using SMT.SagePos.BusinessObjects;
using System.Linq;
using System.Data.Linq;
using System.Collections.Generic;
using System.Reflection;
using System.Diagnostics;

public class VirtualJustInTimeDemo : System.Windows.Forms.Form
{
    private DataGridView dgv = new DataGridView();
    private Cache memoryCache;

    private string table = "Poop";

    protected override void OnLoad(EventArgs e)
    {
        #region init controls
        // Initialize the form.
        this.AutoSize = true;
        this.Controls.Add(this.dgv);
        this.Text = "DataGridView virtual-mode just-in-time demo";

        // Complete the initialization of the DataGridView.
        this.dgv.Size = new Size(800, 250);
        this.dgv.Dock = DockStyle.Fill;
        this.dgv.VirtualMode = true;
        this.dgv.ReadOnly = true;
        this.dgv.AllowUserToAddRows = false;
        this.dgv.AllowUserToOrderColumns = false;
        this.dgv.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
        this.dgv.CellValueNeeded += new DataGridViewCellValueEventHandler(dgv_CellValueNeeded);
        #endregion

        // Create DataRetriever, use it to create Cache object and to initialize DataGridView columns/rows.
        try
        {
            DataRetriever retriever = new DataRetriever(table);
            memoryCache = new Cache(retriever, 2500);

            foreach (string columnName in retriever.Columns)
                dgv.Columns.Add(columnName, columnName);
            
            this.dgv.RowCount = retriever.RowCount;
        }
        catch (SqlException)
        {
            MessageBox.Show("Connection could not be established. Verify that the connection string is valid.");
            Application.Exit();
        }

        // Adjust the column widths based on the displayed values.
        this.dgv.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.DisplayedCells);

        base.OnLoad(e);
    }

    private void dgv_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
    {
        e.Value = memoryCache.RetrieveElement(e.RowIndex, e.ColumnIndex);
        //dgv.Columns[e.ColumnIndex].Name,
    }

    public static void Main()
    {
        Application.Run(new VirtualJustInTimeDemo());
    }
}

public interface IDataPageRetriever
{
    List<BusinessObjectBase> SupplyPageOfData(int lowerPageBoundary, int rowsPerPage);
}

public class DataRetriever : IDataPageRetriever
{
    private string tableName;
    private Customers customers = new Customers();

    public DataRetriever(string tableName)
    {
        this.tableName = tableName;
    }

    private int rowCountValue = -1;

    public int RowCount
    {
        get
        {
            // Return the existing value if it has already been determined.
            if (rowCountValue != -1) return rowCountValue;

            rowCountValue = customers.CountAll();
            return rowCountValue;
        }
    }

    private List<string> columnsValue = new List<string>();

    public List<string> Columns
    {
        get
        {
            // Return the existing value if it has already been determined.
            if (columnsValue.Count > 0) return columnsValue;

            // Retrieve the column information from the database.
            PropertyInfo[] pis = typeof(Customer).GetProperties();
            
            foreach (PropertyInfo c in pis)
                columnsValue.Add(c.Name);

            return columnsValue;
        }
    }

    public List<BusinessObjectBase> SupplyPageOfData(int lowerPageBoundary, int rowsPerPage)
    {
        // Retrieve the specified # of rows from db, starting w/ row specified by lowerPageBoundary.
        
        //IEnumerable<Object> enumerable = pdc.Customers.Skip(lowerPageBoundary).Take(rowsPerPage).OrderBy(o => (o as Customer).cus_id);
        customers.LoadPage(lowerPageBoundary, rowsPerPage, "id");
        
        //List<Object> list = enumerable.ToList();

        return customers.ToList();
    }

}

public static class PropInfos
{
    public static PropertyInfo[] pis(object obj)
    {
        return obj.GetType().GetProperties();
    }
}

public class Cache
{
    private static int RowsPerPage;

    // Represents one page of data.  
    public struct DataPage
    {
        public List<BusinessObjectBase> table;

        public int LowestIndex  { get; private set; }
        public int HighestIndex { get; private set; }

        public DataPage(List<BusinessObjectBase> table, int rowIndex) : this()
        {
            this.table = table;
            LowestIndex  = MapToLowerBoundary(rowIndex);
            HighestIndex = MapToUpperBoundary(rowIndex);
            System.Diagnostics.Debug.Assert(LowestIndex >= 0);
            System.Diagnostics.Debug.Assert(HighestIndex >= 0);
        }

        // Return the lowest index of a page containing the given index.
        public  static int MapToLowerBoundary(int rowIndex) { return (rowIndex / RowsPerPage) * RowsPerPage; }

        // Return the highest index of a page containing the given index.
        private static int MapToUpperBoundary(int rowIndex) { return MapToLowerBoundary(rowIndex) + RowsPerPage - 1; }
    }

    private DataPage[] cachePages;
    private IDataPageRetriever dataSupply;

    public Cache(IDataPageRetriever dataSupplier, int rowsPerPage)
    {
        dataSupply = dataSupplier;
        Cache.RowsPerPage = rowsPerPage;
        LoadFirstTwoPages();
    }

    // Sets the value of the element parameter if the value is in the cache.
    private bool IfPageCached_ThenSetElement(int rowIndex, int columnIndex, ref string element)
    {
        if (IsRowCachedInPage(0, rowIndex))
        {
            int row = rowIndex % RowsPerPage;
            int col = columnIndex;

            //order by, skip
            BusinessObjectBase r = cachePages[0].table.Skip(row).Take(1).Single();
            
            //get the value from the selected column
            PropertyInfo colInfo = PropInfos.pis(r)[col];
            var o = colInfo.GetValue(r, null);
            
            string v;
            if (o == null) v = null; else v = o.ToString();
            
            element = v;
            return true;
        }
        else if (IsRowCachedInPage(1, rowIndex))
        {
            int row = rowIndex % RowsPerPage;
            int col = columnIndex;

            //order by, skip
            BusinessObjectBase r = cachePages[1].table.Skip(row).Take(1).Single();

            //get the value from the selected column
            PropertyInfo colInfo = PropInfos.pis(r)[col];

            var o = colInfo.GetValue(r, null);

            string v;
            if (o == null) v = null; else v = o.ToString();

            element = v;
            return true;
        }

        return false;
    }

    public string RetrieveElement(int rowIndex, int columnIndex)
    {
        string element = null;

        if (IfPageCached_ThenSetElement(rowIndex, columnIndex, ref element))
            return element;
        else
            return RetrieveData_CacheIt_ThenReturnElement(rowIndex, columnIndex);
    }

    private void LoadFirstTwoPages()
    {
        cachePages = new DataPage[]
        {
            new DataPage(dataSupply.SupplyPageOfData(DataPage.MapToLowerBoundary(0), RowsPerPage), 0), 
            new DataPage(dataSupply.SupplyPageOfData(DataPage.MapToLowerBoundary(RowsPerPage), RowsPerPage), RowsPerPage)
        };
    }

    private string RetrieveData_CacheIt_ThenReturnElement(int rowIndex, int columnIndex)
    {
        // Retrieve a page worth of data containing the requested value.
        List<BusinessObjectBase> table = dataSupply.SupplyPageOfData(DataPage.MapToLowerBoundary(rowIndex), RowsPerPage);

        // Replace the cached page furthest from the requested cell
        // with a new page containing the newly retrieved data.
        cachePages[GetIndexToUnusedPage(rowIndex)] = new DataPage(table, rowIndex);

        return RetrieveElement(rowIndex, columnIndex);
    }

    // Returns the index of the cached page most distant from the given index
    // and therefore least likely to be reused.
    private int GetIndexToUnusedPage(int rowIndex)
    {
        if (rowIndex > cachePages[0].HighestIndex && //row index is > highest index in both cached pages
            rowIndex > cachePages[1].HighestIndex)
        {
            int offsetFromPage0 = rowIndex - cachePages[0].HighestIndex;
            int offsetFromPage1 = rowIndex - cachePages[1].HighestIndex;
            if (offsetFromPage0 < offsetFromPage1) return 1;
            return 0;
        }
        else //row index is < highest index of one or both cached pages. (not <= b/c if it were = this method wouldn't be called)
        {
            int offsetFromPage0 = cachePages[0].LowestIndex - rowIndex;
            int offsetFromPage1 = cachePages[1].LowestIndex - rowIndex;
            if (offsetFromPage0 < offsetFromPage1) return 1;
            return 0;
        }

    }

    // Returns a value indicating whether the given row index is contained in the given DataPage. 
    private bool IsRowCachedInPage(int pageNumber, int rowIndex)
    {
        return rowIndex <= cachePages[pageNumber].HighestIndex &&
               rowIndex >= cachePages[pageNumber].LowestIndex;
    }

}

